# Copyright 2007 Pete Lypkie, Jonathon Trivett
# This file is part of Luqi
#
# Luqi is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Luqi is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Luqi; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

import CamImage
import random
from datetime import datetime

LuqiVersion = "Luqi webcam weiqi recorder, version 0.1"

class GameMetadata:

    def __init__(self):
        self.playernames = ["", ""]
        self.playerranks = ["", ""] # should be "15k" or "3d", for example
        self.komi = "6.5"
        self.handicap = "0"
        self.time = "0"  #seconds, 0 for infinite
        self.date = datetime.today()
        self.event = ""
        self.rules = "Japanese"
        self.user = LuqiVersion
        


class Qipan:
    """This class holds the internal representation of a Weiqi board.

    Inside, you will find a function to update the board state given a 
    new array of average pixel values (basically blurring an area down to
    average value).  It should keep old average pixel arrays in order to 
    compare and figure out where the new moves were played.

    This class is also responsible for giving out an SGF representation
    of the game, and for communicating via GTP (Go Text Protocol)."""

    def __init__(self):
        self._gamestate = GameState()
        self._gametree = GameTree()
        self._lastavgarray = None
        self._metadata = GameMetadata()

        
    # Notes:
    # I'm thinkin that there should be an internal tree structure
    # made out of lists, which would be the entire move history
    # (possibly including variations).  each node in the list would 
    # be a tuple with the player "B" or "W" and a position noted by
    # using latin letters in order with none skipped (ie "aa", "ab"
    # up to "as" for the first row 19th column.

    # initially, moves may be added to the move tree without checking, 
    # but in the future they should be checked against the current
    # board state so that illegal moves can't be played and could be
    # placed in the next-best guessed position.

    # new info will be added by receiving processed shots of the
    # board.  The first method will be averaged squares, where each
    # square on the board will have its pixels averaged and put into a
    # 2D array.  the Qipan function to accept these will be
    # Qipan.nextarray(array).  this function will process it and find
    # one or more moves to add to the game tree.  it will also update
    # the gamestate.

    # reference for SGF format: http://www.red-bean.com/sgf/

    def nextavgarray(self, array):
        """Accept a 2D array of average colors for board positions"""

        # process the array and compare to previous arrays to
        # determine where the next move should be placed on the
        # board.  

        #simple method:  find a spot that's different, and it must be
        #the spot where the next stone is.
        position = self.most_different(array, self._lastavgarray)
        self._lastavgarray = array        
        self.nextmove(position)


    def nextmove(self, position):
        """Performs all necessary things before the next move

        If we're doing many things like outputing to an SGF, updating
        the internal representation, outputting info via Go Text
        Protocol, etc, then it will all be done in here"""
        
        assert(isinstance(position, str))
        assert(len(position) == 2)
        assert(position.isalpha())
        assert(position.islower())

        self._gametree.setnext(position, self._gamestate.nextplayer)
        self._gamestate.setnext(position)  #toggles nextplayer



    def most_different(self, newarray, oldarray):
        # Note: just faking it with random positions right now
        #letters 'a' to 's':
        letters = [chr(i+ord('a')) for i in xrange(19)]
        random.seed()
        x = letters[random.randint(0,18)]
        y = letters[random.randint(0,18)]
        return x + y


    def getSGF(self):
        return self._gametree.getSGF(self._metadata)

    



#GameState constants:
_GSEmpty = 0 
_GSBlack = 1 
_GSWhite = 2 
_GSKo = 3  # ko


class GameState:
    """stores the current board position"""

    def __init__(self):
        """initialize the board to empty and set black first"""
        self._state = []
        for x in xrange(19):
            for y in xrange(19):
                self._state[x][y] = _GSEmpty
        self.nextplayer = _GSBlack

    def togglenext(self):
        """if black moved, then toggle to white, and vice-versa"""
        if self.nextplayer == _GSBlack:
            self.nextplayer = _GSWhite
        else:
            self.nextplayer = _GSBlack

    def setnext(self, position):
        """next move was performed at position 

        position is given in "ab" format where "ab" = first row,
        second column according to alphabetic order. 

        this assumes that the next move always alternates
        and it will automatically toggle the next move color"""

        x, y = position # split string into 2 chars
        self._state[self.ord(x)][self.ord(y)] = self.nextplayer
        self.togglenext()


    def ord(self, char):
        """change 'a' to 0, 'b' to 1, etc up to 's' = 18"""
        return ord(char) - ord('a')  # using builtin ord, ord('a') == 97
    


class GameTree:
    """This class stores the tree of moves that have occurred so far.

    It will support multiple branches and rewinding of moves.  Later,
    it will be used to convert the game to SGF by doing in-order
    traversal."""

    def __init__(self):
        self._tree = []
        self._cursor = self._tree
        


    def setnext(self, position, player):
        """adds the next move to the tree, following the current branch.

        position is expected as a length-2 string of letters,
        representing the row and column"""

        # each tree node will be a tuple of color with position.  it's
        # possible that in the future, the tuple could include more
        # elements after that, such as comments or something...dunno
        # what right now.  

        # it should be able to support multiple branches and current
        # game positions that aren't at the end of the list.  So we'll
        # need a branch pointer and a cursor, and if setnext is called
        # when the cursor isn't at the end of the branch then it
        # should make a new branch and set the cursor at the end of
        # that.  perhaps have a boolean to indicate whether the cursor
        # is at the end of the branch just to make it simpler.  then
        # if there's a rewind function then it could toggle the
        # variable when it happens instead of this function constantly
        # computing it.

        # FIXME: no branching right now, just straight moves in one
        # branch.  Probably need to implement doubly-linked list in
        # the future to allow going back to previous moves easily
        
        self._tree.append((player, position))


    
# example tree, except with B and W constants that don't exist:
# one branch:     [ (B, "aa"), (W, "bb"), (B, "ac"), (W, "jh") ]
# one alternative after 2nd move:  
#  [ (B, "aa"), (W, "bb"), [[(B, "ad"), (W, "jj")],[(B, "ac"), (W,
# "jh")]]

# So, if there's a branch, then the whole rest of the game is its own
# list.  Inside that list are multiple possible branches.  each branch
# is its own list.  so [ a, b, [[c]] ] is the same as [ a, b, c ]
# To make a branch, you go to the move that will have the starting
# alternative, and you take the whole rest of the list from there on
# and put it into a sub-list, like c above.  then after [c], you can
# put a list with the new branch inside it.  At the branch point,
# there's basically a list consisting of all branches that start
# there. 

# if you have a game with 5 moves and then you want to make a new
# branch at the 3rd move, with the new branch having a total of 6
# moves after the branch point, then it'd go like this:
# [a, b, c, d, e]  (5 moves, no branches)
# [a, b, [[c, d, e]] (still 5 moves, preparing to branch)
# [a, b, [[c, d, e], [f, g, h, i, j, k]]] (played f instead of c at
# move 3, and then continued on.)

# It's built this way so that the normal situation where there's no
# branching makes a nice list with no sublists. it also mirrors the
# way SGF files are constructed, and enables the quick movement
# through the main branch because the main branch will be completely
# listed first.  Nodes are listed as an in-order traversal, so you
# don't need to back up through the list to do this.


    def getSGF(self, metadata):
        """Produces an SGF from the current game state"""
        sgf = ""
        sgf += "(;FF[4]GM[1]SZ[19]"  # file format version 4
        sgf += "US[%s]" % metadata.user  #software name
        if len(metadata.playernames[0]) > 0:
            sgf += "PB[%]" % metadata.playernames[0]
        if len(metadata.playernames[1]) > 0:
            sgf += "PW[%s]" % metadata.playernames[1]
        if len(metadata.playerranks[0]) > 0:
            sgf += "BR[%s]" % metadata.playerranks[0]
        if len(metadata.playerranks[1]) > 0:
            sgf += "WR[%s]" % metadata.playerranks[1]
        # what about RE[] for result of the game?
        sgf += "KM[%s]" % metadata.komi
        sgf += "HA[%s]" % metadata.handicap
        sgf += "TM[%s]" % metadata.time
        date = metadata.date
        sgf += "DT[%4i-%2i-%2i]" % (date.year, date.month, date.day)
        sgf += "EV[%s]" % metadata.event
        sgf += "RU[%s]" % metadata.rules
        
        # done sgf metadata.
        # now, convert the tree:
        sgf += self.node2sgf(self._tree, True)  #True means top of tree
        sgf += ")"
        return sgf

    #end of getSGF

        
    def node2sgf(self, node, top=False):
        if isinstance(node, list):
            if isinstance(node[0], list):
                # this is a branch point.  make some parentheses and keep
                # going.
                # go through and produce SGF for all of the contents of
                # this node because it's a list of branches.
                # Note that parentheses are not added here because
                # each branch will get its own parentheses and this is
                # just a list of branches.
                return reduce(lambda x, y: x + self.node2sgf(y), 
                              node, 
                              "")
            else:
                # could be a list that's an element of a branchlist, but
                # isn't a branchlist itself.  Add parentheses and then
                # proceed, basically just like this is the start of a
                # new tree.
                if top == False:
                    return "(" + reduce(lambda x, y: x + self.node2sgf(y), 
                                        node,
                                        "") + ")"
                else:
                    # if it's the top of the game tree, then don't put 
                    # parens around all the moves.
                    return reduce(lambda x, y: x + self.node2sgf(y),
                                  node,
                                  "")
        # not a list of lists (branchpoint), not a list, check
        # if it's a tuple
        if isinstance(node, tuple):
            # base case of node2sgf
            sgf = ";"
            if node[0] == _GSBlack:
                sgf += "B["
            else:
                if node[0] == _GSWhite:
                    sgf += "W["
                else:
                    pass # FIXME: throw an error about unknown move type
            if isinstance(node[1], str) and len(node[1]) == 2:
                sgf += node[1] + "]"
            else:
                pass # FIXME: throw an error about invalid position
            return sgf
        else:
            pass # FIXME: invalid tree, no tuple at base case.  throw
                 # an error
    #end of node2sgf



